/**
 *    Copyright 2009-2018 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.yinsin.jpabatis.executor;

import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;

import javax.persistence.EntityManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;

import com.yinsin.jpabatis.cache.CacheKey;
import com.yinsin.jpabatis.cache.impl.PerpetualCache;
import com.yinsin.jpabatis.config.Configuration;
import com.yinsin.jpabatis.mapper.BoundSql;
import com.yinsin.jpabatis.mapper.MappedStatement;
import com.yinsin.jpabatis.mapper.ParameterMapping;
import com.yinsin.jpabatis.mapper.ParameterMode;
import com.yinsin.jpabatis.mapper.StatementType;
import com.yinsin.jpabatis.reflection.MetaObject;
import com.yinsin.jpabatis.reflection.factory.ObjectFactory;
import com.yinsin.jpabatis.session.LocalCacheScope;
import com.yinsin.jpabatis.session.ResultHandler;
import com.yinsin.jpabatis.session.RowBounds;
import com.yinsin.jpabatis.type.TypeHandlerRegistry;

/**
 * @author Clinton Begin
 */
public abstract class BaseExecutor implements Executor {

	private static final Logger log = LoggerFactory.getLogger(BaseExecutor.class);

	protected EntityManager transaction;
	protected Executor wrapper;

	protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
	protected PerpetualCache localCache;
	protected PerpetualCache localOutputParameterCache;
	protected Configuration configuration;

	protected int queryStack;
	private boolean closed;

	protected BaseExecutor(Configuration configuration, EntityManager transaction) {
		this.transaction = transaction;
		this.deferredLoads = new ConcurrentLinkedQueue<>();
		this.localCache = new PerpetualCache("LocalCache");
		this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
		this.closed = false;
		this.configuration = configuration;
		this.wrapper = this;
	}

	@Override
	public EntityManager getTransaction() {
		if (closed) {
			throw new ExecutorException("Executor was closed.");
		}
		return transaction;
	}

	@Override
	public boolean isClose() {
		return !transaction.isOpen();
	}

	@Override
	public <E> E query(MappedStatement ms, Object parameter, ResultHandler<?> resultHandler) throws SQLException {
		BoundSql boundSql = ms.getBoundSql(parameter);
		CacheKey key = createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql);
		return query(ms, parameter, resultHandler, key, boundSql);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <E> E query(MappedStatement ms, Object parameter, ResultHandler<?> resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
		ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
		if (closed) {
			throw new ExecutorException("Executor was closed.");
		}
		if (queryStack == 0 && ms.isFlushCacheRequired()) {
			clearLocalCache();
		}
		List<E> list;
		try {
			queryStack++;
			list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
			if (list != null) {
				handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
			} else {
				list = queryList(ms, parameter, resultHandler, key, boundSql);
			}
		} finally {
			queryStack--;
		}
		if (queryStack == 0) {
			for (DeferredLoad deferredLoad : deferredLoads) {
				deferredLoad.load();
			}
			// issue #601
			deferredLoads.clear();
			if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
				// issue #482
				clearLocalCache();
			}
		}
		return list.get(0);
	}
	
	@Override
	public <E> List<E> queryList(MappedStatement ms, Object parameter, ResultHandler<?> resultHandler) throws SQLException {
		BoundSql boundSql = ms.getBoundSql(parameter);
		CacheKey key = createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql);
		return queryList(ms, parameter, resultHandler, key, boundSql);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <E> List<E> queryList(MappedStatement ms, Object parameter, ResultHandler<?> resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
		ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
		if (closed) {
			throw new ExecutorException("Executor was closed.");
		}
		if (queryStack == 0 && ms.isFlushCacheRequired()) {
			clearLocalCache();
		}
		List<E> list;
		try {
			queryStack++;
			list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
			if (list != null) {
				handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
			}
		} finally {
			queryStack--;
		}
		if (queryStack == 0) {
			for (DeferredLoad deferredLoad : deferredLoads) {
				deferredLoad.load();
			}
			// issue #601
			deferredLoads.clear();
			if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
				// issue #482
				clearLocalCache();
			}
		}
		return list;
	}
	
	@Override
	public <E> Page<E> queryList(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler<?> resultHandler) throws SQLException {
		BoundSql boundSql = ms.getBoundSql(parameter);
		CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
		return queryList(ms, parameter, rowBounds, resultHandler, key, boundSql);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <E> Page<E> queryList(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler<?> resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
		ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
		if (closed) {
			throw new ExecutorException("Executor was closed.");
		}
		if (queryStack == 0 && ms.isFlushCacheRequired()) {
			clearLocalCache();
		}
		Page<E> list;
		try {
			queryStack++;
			list = resultHandler == null ? (Page<E>) localCache.getObject(key) : null;
			if (list != null) {
				handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
			}
		} finally {
			queryStack--;
		}
		if (queryStack == 0) {
			for (DeferredLoad deferredLoad : deferredLoads) {
				deferredLoad.load();
			}
			// issue #601
			deferredLoads.clear();
			if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
				// issue #482
				clearLocalCache();
			}
		}
		return list;
	}
	
	public abstract <E> E doQuery(MappedStatement ms, Object parameter, ResultHandler<?> resultHandler, BoundSql boundSql) throws SQLException;
	
	public abstract <E> List<E> doQueryList(MappedStatement ms, Object parameter, ResultHandler<?> resultHandler, BoundSql boundSql) throws SQLException;
	
	public abstract <E> Page<E> doQueryList(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler<?> resultHandler, BoundSql boundSql) throws SQLException;

	@Override
	public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
		if (closed) {
			throw new ExecutorException("Executor was closed.");
		}
		CacheKey cacheKey = new CacheKey();
		cacheKey.update(ms.getId());
		cacheKey.update(rowBounds.getOffset());
		cacheKey.update(rowBounds.getLimit());
		cacheKey.update(boundSql.getSql());
		List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
		TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
		// mimic DefaultParameterHandler logic
		for (ParameterMapping parameterMapping : parameterMappings) {
			if (parameterMapping.getMode() != ParameterMode.OUT) {
				Object value;
				String propertyName = parameterMapping.getProperty();
				if (boundSql.hasAdditionalParameter(propertyName)) {
					value = boundSql.getAdditionalParameter(propertyName);
				} else if (parameterObject == null) {
					value = null;
				} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
					value = parameterObject;
				} else {
					MetaObject metaObject = configuration.newMetaObject(parameterObject);
					value = metaObject.getValue(propertyName);
				}
				cacheKey.update(value);
			}
		}
		return cacheKey;
	}

	@Override
	public boolean isCached(MappedStatement ms, CacheKey key) {
		return localCache.getObject(key) != null;
	}

	@Override
	public void clearLocalCache() {
		if (!closed) {
			localCache.clear();
			localOutputParameterCache.clear();
		}
	}

	protected void closeStatement(Statement statement) {
		if (statement != null) {
			try {
				statement.close();
			} catch (SQLException e) {
				// ignore
			}
		}
	}

	private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
		if (ms.getStatementType() == StatementType.CALLABLE) {
			final Object cachedParameter = localOutputParameterCache.getObject(key);
			if (cachedParameter != null && parameter != null) {
				final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
				final MetaObject metaParameter = configuration.newMetaObject(parameter);
				for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
					if (parameterMapping.getMode() != ParameterMode.IN) {
						final String parameterName = parameterMapping.getProperty();
						final Object cachedValue = metaCachedParameter.getValue(parameterName);
						metaParameter.setValue(parameterName, cachedValue);
					}
				}
			}
		}
	}

	@Override
	public void setExecutorWrapper(Executor wrapper) {
		this.wrapper = wrapper;
	}

	private static class DeferredLoad {

		private final MetaObject resultObject;
		private final String property;
		private final Class<?> targetType;
		private final CacheKey key;
		private final PerpetualCache localCache;
		private final ObjectFactory objectFactory;
		private final ResultExtractor resultExtractor;

		// issue #781
		public DeferredLoad(MetaObject resultObject, String property, CacheKey key, PerpetualCache localCache, Configuration configuration, Class<?> targetType) {
			this.resultObject = resultObject;
			this.property = property;
			this.key = key;
			this.localCache = localCache;
			this.objectFactory = configuration.getObjectFactory();
			this.resultExtractor = new ResultExtractor(configuration, objectFactory);
			this.targetType = targetType;
		}

		public boolean canLoad() {
			return localCache.getObject(key) != null && localCache.getObject(key) != ExecutionPlaceholder.EXECUTION_PLACEHOLDER;
		}

		public void load() {
			@SuppressWarnings("unchecked")
			// we suppose we get back a List
			List<Object> list = (List<Object>) localCache.getObject(key);
			Object value = resultExtractor.extractObjectFromList(list, targetType);
			resultObject.setValue(property, value);
		}

	}

}
